package com.chenjunlin.app.service.Impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.http.server.HttpServerResponse;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.chenjunlin.app.common.constant.LinkConstant;
import com.chenjunlin.app.common.convention.errorcode.ServiceErrorCode;
import com.chenjunlin.app.common.convention.exception.ServiceException;
import com.chenjunlin.app.common.enums.validTypeEnum;
import com.chenjunlin.app.common.errorcode.LinkErrorCodeEnun;
import com.chenjunlin.app.dao.entity.LinkDO;
import com.chenjunlin.app.dao.mapper.LinkMapper;
import com.chenjunlin.app.dto.reqtDTO.AddLinkReqDTO;
import com.chenjunlin.app.dto.reqtDTO.PageLinkReqDTO;
import com.chenjunlin.app.dto.reqtDTO.UpdateLinkReqDTO;
import com.chenjunlin.app.dto.resp.AddLinkRespDTO;
import com.chenjunlin.app.dto.resp.PageLinkRespDTO;
import com.chenjunlin.app.service.LinkService;
import com.chenjunlin.app.util.HashUtil;
import com.sun.net.httpserver.HttpServer;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.redisson.Redisson;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.BeanUtils;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.util.Objects;
import java.util.concurrent.TimeUnit;

@Service
@RequiredArgsConstructor
public class LinkServiceImpl extends ServiceImpl<LinkMapper, LinkDO> implements LinkService {

    private final RBloomFilter bloomfilter;

    private final RedisTemplate redisTemplate;

    private final RedissonClient redissonClient;
    /**
     * 新增短链接
     * @param addLinkReqDTO
     * @return
     */
    @Override
    public AddLinkRespDTO addLink(AddLinkReqDTO addLinkReqDTO) {
        String suffix = generateSuffix(addLinkReqDTO);
        String fullShortUrl = LinkConstant.domain + suffix;

        LinkDO linkDO = new LinkDO();
        BeanUtils.copyProperties(addLinkReqDTO,linkDO);
        linkDO.setFullShortUrl(fullShortUrl);
        linkDO.setShortUri(suffix);
        linkDO.setDomain(LinkConstant.domain);
        linkDO.setEnableStatus(0);

        try {
            baseMapper.insert(linkDO);
        } catch (DuplicateKeyException ex) {
            //捕获重复插入异常
            LambdaQueryWrapper<LinkDO> wrapper = Wrappers.lambdaQuery(LinkDO.class)
                    .eq(LinkDO::getFullShortUrl, fullShortUrl);
            LinkDO link = baseMapper.selectOne(wrapper);
            if(link != null) {
                throw new ServiceException(LinkErrorCodeEnun.FULL_LINK_ALREADY_EXISTS);
            }
        }
        //成功插入后加入改映射到redis,进行缓存预热
        redisTemplate.opsForValue().set(String.format(LinkConstant.SHORT_TO_ORIGINAL_KEY,fullShortUrl),linkDO.getOriginUrl());
        //布隆过滤器存储完整短链接信息,减少一次mysql访问，防止短链接重复插入
        bloomfilter.add(fullShortUrl);

        AddLinkRespDTO reuslt = AddLinkRespDTO.builder()
                .gid(linkDO.getGid())
                .fullShortUrl(linkDO.getFullShortUrl())
                .originUrl(linkDO.getOriginUrl())
                .build();
        return reuslt;
    }

    /**
     * 分页查询短链接信息
     * @param pageLinkReqDTO
     * @return
     */
    public IPage<PageLinkRespDTO> pageQuery(PageLinkReqDTO pageLinkReqDTO) {
        LambdaQueryWrapper<LinkDO> wrapper = Wrappers.lambdaQuery(LinkDO.class)
                .eq(LinkDO::getGid, pageLinkReqDTO.getGid())
                .eq(LinkDO::getDelFlag, 0)
                .eq(LinkDO::getEnableStatus, 0);
        IPage<LinkDO> resultPage = baseMapper.selectPage(pageLinkReqDTO, wrapper);
        return resultPage.convert(each -> BeanUtil.toBean(each, PageLinkRespDTO.class));

    }

    /**
     * 修改短链接操作
     * @param updateLinkReqDTO
     * @return
     */
    @Override
    public void updateByDTO(UpdateLinkReqDTO updateLinkReqDTO) {
        LambdaQueryWrapper<LinkDO> wrapper = Wrappers.lambdaQuery(LinkDO.class)
                .eq(LinkDO::getFullShortUrl, updateLinkReqDTO.getFullShortUrl())
                .eq(LinkDO::getDelFlag, 0)
                .eq(LinkDO::getEnableStatus, 0);
        LinkDO linkDO = baseMapper.selectOne(wrapper);

        if(linkDO == null){
            throw new ServiceException(LinkErrorCodeEnun.LINK_NOT_FOUND);
        }

        LinkDO resultLink = LinkDO.builder()
                .shortUri(linkDO.getShortUri())
                .clickNum(linkDO.getClickNum())
                .domain(updateLinkReqDTO.getDomain())
                .originUrl(updateLinkReqDTO.getOriginUrl())
                .enableStatus(updateLinkReqDTO.getEnableStatus())
                .validDate(updateLinkReqDTO.getValidDate())
                .validDateType(updateLinkReqDTO.getValidDateType())
                .describe(updateLinkReqDTO.getDescribe())
                .build();

        LambdaUpdateWrapper<LinkDO> updateWrapper = Wrappers.lambdaUpdate(LinkDO.class)
                .eq(LinkDO::getFullShortUrl, updateLinkReqDTO.getFullShortUrl())
                .set(Objects.equals(updateLinkReqDTO.getValidDateType(), validTypeEnum.PERMANENT.getType()), LinkDO::getValidDateType, null);
        baseMapper.update(resultLink,updateWrapper);


    }

    @SneakyThrows
    @Override
    public void redirect(String shortUri, ServletRequest request, ServletResponse response) {
        String full_short_url = LinkConstant.domain + shortUri;

        if(!bloomfilter.contains(full_short_url)) {
            //1.如果布隆过滤器里没有这个记录，则数据库一定没有,直接抛404
            ((HttpServletResponse) response).sendRedirect("/page/notfound");
            return;
        }
        String original_link;
        //2.查询是否有空值缓存,如果有则直接返回 解决缓存穿透的问题
        original_link = (String) redisTemplate.opsForValue().get(String.format(LinkConstant.SHORT_OF_NO_ORIGINAL_KEY,full_short_url));
        if(StrUtil.isNotBlank(original_link)){
            ((HttpServletResponse) response).sendRedirect("/page/notfound");
            //TODO 如果有对应的空值缓存跳转404页面
            return;
        }
        //如果布隆过滤器里有则直接查缓存/数据库
        //3.先查redis缓存,如果命中 则直接跳转
        original_link =(String) redisTemplate.opsForValue()
                                .get(String.format(LinkConstant.SHORT_TO_ORIGINAL_KEY, full_short_url));
        if(StrUtil.isNotBlank(original_link)){
            ((HttpServletResponse) response).sendRedirect(original_link);
            return;
        }

        //4.redis未命中,先查数据库，再加入redis,再跳转
        //查数据库,未防止热点key失效后的大量查询,使用双重判定的分布式锁解决缓存击穿的问题
        RLock lock = redissonClient.getLock(String.format(LinkConstant.LOCK_SHORT_TO_ORIGINAL_KEY, full_short_url));
        lock.lock();
        try {
            //(双重判定)获取分布锁之后 再次查询redis,解决多个请求同时请求同一数据,第一个数据成功之后后面的直接查询缓存 可加速跳转
            original_link =(String) redisTemplate.opsForValue()
                                                 .get(String.format(LinkConstant.SHORT_TO_ORIGINAL_KEY, full_short_url));
            if(StrUtil.isNotBlank(original_link)){
                ((HttpServletResponse) response).sendRedirect(original_link);
                return;
            }

            //获取分布式锁后双重判断是否有空值缓存,解决缓存击穿问题(大量mysql中不存在,且不同的key请求会导致mysql压力 以及redis崩溃)
            original_link = (String) redisTemplate.opsForValue().get(String.format(LinkConstant.SHORT_OF_NO_ORIGINAL_KEY,full_short_url));
            if(StrUtil.isNotBlank(original_link)){
                // 如果有对应的空值缓存跳转404页面
            ((HttpServletResponse) response).sendRedirect("/page/notfound");
                return;
            }

            LambdaQueryWrapper<LinkDO> lambdaQueryWrapper = Wrappers.lambdaQuery(LinkDO.class)
                                                                    .eq(LinkDO::getFullShortUrl, full_short_url)
                                                                    .eq(LinkDO::getDelFlag, 0)
                                                                    .eq(LinkDO::getEnableStatus, 0);
            LinkDO linkDO = baseMapper.selectOne(lambdaQueryWrapper);
            if(linkDO == null){
                //没查到数据,设置对应的空缓存防止缓存穿透
                redisTemplate.opsForValue().set(String.format(LinkConstant.SHORT_TO_ORIGINAL_KEY, full_short_url),"No-data",30L, TimeUnit.SECONDS);
                ((HttpServletResponse) response).sendRedirect("/page/notfound");
                return;
            }
            //mysql查询成功,存入redis
            redisTemplate.opsForValue().set(String.format(LinkConstant.SHORT_TO_ORIGINAL_KEY, full_short_url),linkDO.getOriginUrl(),30L,TimeUnit.MINUTES);
            ((HttpServletResponse) response).sendRedirect(linkDO.getOriginUrl());
        }finally {
            lock.unlock();
        }

    }

    /**
     * 根据目的连接生成 短链接的后缀
     */
    private String generateSuffix(AddLinkReqDTO addLinkReqDTO){
        String originUrl = addLinkReqDTO.getOriginUrl();
        String suffix;
        int retryCount = 0;
        while(true){
            if(retryCount >= 10){
                throw new ServiceException(LinkErrorCodeEnun.GENERATE_SAME_ALWAYS);
            }
            String TimeOriginUrl = originUrl + System.currentTimeMillis();
            suffix = HashUtil.hashToBase62(TimeOriginUrl);
            String fullShortUrl = LinkConstant.domain + suffix;
            if(!bloomfilter.contains(fullShortUrl)){
                break;
            }
            retryCount++;
        }

        return suffix;
    }

}
